/* ***** BEGIN LICENSE BLOCK *****
 *   Version: GPL 2.0
 *
 * Bookstack extension: a queue implementation for bookmarks.
 * Copyright (C) 2007-2011  Adam Dane
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ***** END LICENSE BLOCK ***** */

"use strict";

var bookstack, bookstackOverlay;

if (typeof bookstack === "undefined") {
  bookstack = {};
}

if (typeof bookstackOverlay === "undefined") {
  bookstackOverlay = {};
}

(function () {
  var Cu = Components.utils,
      pref,
      stack,
      serv,
      BMSVC;

  try {
    Cu["import"]("resource://bookstack/bookstack.jsm");
    pref = bookstack.pref;
    stack = bookstack.stack;
    serv = bookstack.serv;
    Cu["import"]("resource://bookstack/bookstack-Drag.jsm");
    bookstack.pref = pref;
    bookstack.stack = stack;
    bookstack.serv = serv;
  }
  catch (exBookstack) {}

  BMSVC = bookstack.serv.BMSVC();

  bookstackOverlay = {
    // Changes whether middle clicks on links are listened.
    toggleMiddleClickListener: function () {
      var action = window.removeEventListener;
      if (bookstack.pref.STACK_MIDCLICK.value()) {
        action = window.addEventListener;
      }
      action("click", bookstackOverlay.usurpMiddleClick, true);
    },

    // Handle the command line via some temporary-use preferences.
    fromCommandLine: function () {
      if (bookstack.pref.STACK_CMDLINE_UPDATE.value()) {
        var url = bookstack.pref.STACK_CMDLINE_URI.value();
        if (url) {
          bookstack.stack.onAddToStackIndex({
            url: url,
            edit: false
          });
        }
        bookstack.pref.STACK_CMDLINE_UPDATE.persist(false);
      }
    },

    // Minimal DND handler for the sidebar button.
    dragObserver: {
      onDragOver: function (aEvent) {
        if (bookstack.dnd.canDrop(aEvent, aEvent.target.id)) {
          return aEvent.preventDefault();
        }
        return false;
      },
      onDrop: function (aEvent) {
        bookstack.dnd.onDrop(
          aEvent, BMSVC.DEFAULT_INDEX, bookstack.stack.getShelf());
      }
    },

    // Boolean: Whether the displayed page is bookmarked in the stack.
    currentIsBookmarked: function () {
      var currentTab = bookstack.serv.getBrowser().mCurrentTab,
          currentURL = currentTab.linkedBrowser.webNavigation.currentURI;
      if (!BMSVC.isBookmarked(currentURL)) {
        return false;
      }
      return BMSVC.getBookmarkIdsForURI(currentURL, {}).some(function(aId) {
        return (BMSVC.getFolderIdForItem(aId) === bookstack.stack.getShelf());
      });
    },

    // Visibility of context menu items based on preferences.
    bookstackContext: function (aEvent) {
      var onLink = gContextMenu.onLink,
          onTextInput = gContextMenu.onTextInput,
          addMenu = document.getElementById("context-bookstack-add"),
          curMenu = document.getElementById("context-bookstack-current"),
          remMenu = document.getElementById("context-bookstack-rcurrent"),
          currentInStack = bookstackOverlay.currentIsBookmarked();
      addMenu.hidden = !onLink ||
                       !bookstack.pref.MENU_CONTENT.value();
      curMenu.hidden = onLink ||
                       onTextInput ||
                       !bookstack.pref.MENU_CURRENT.value() ||
                       currentInStack;
      remMenu.hidden = onLink ||
                       onTextInput ||
                       !bookstack.pref.MENU_RCURRENT.value() ||
                       !currentInStack;
    },

    // Visibility of settings menu item on the tools menu based on preference.
    bookstackTools: function () {
      document.getElementById("bookstack-tool").hidden =
        !bookstack.pref.MENU_TOOLS.value();
    },

    // Wrapper for the built-in bookmark editor dialog.
    properties: function (aItemId, aType) {
      aItemId = parseInt(aItemId, 10);
      if (isNaN(aItemId)) {
        return;
      }
      var info = {
        action: "edit",
        type: aType,
        itemId: aItemId
      };
      return PlacesUIUtils.showBookmarkDialog(info, document.defaultView) ||
             false;
    },

    // Content area context item's associated function to add link to stack.
    menuAddToStack: function (aEvent) {
      return bookstack.stack.onAddToStackIndex({
        url: gContextMenu.linkURL,
        title: gContextMenu.linkText(),
        edit: !aEvent ? false : aEvent.shiftKey
      });
    },

    // Content area context: add this page to stack
    addCurrentToStack: function (aEvent) {
      var ctrl = !aEvent ? false : aEvent.ctrlKey,
          closePref = bookstack.pref.STACK_CLOSEADD.value(),
          // Equivalent to XOR
          wantToClose = ((closePref || ctrl) && !(closePref && ctrl)),
          browser = bookstack.serv.getBrowser(),
          context = browser.mCurrentTab.linkedBrowser,
          // Manually check the number of tabs; don't close last tab.
          canClose = (browser.browsers.length > 1),
          newBookmark = bookstack.stack.onAddToStackIndex({
            uri: context.webNavigation.currentURI,
            title: context.contentDocument.title,
            edit: !aEvent ? false : aEvent.shiftKey
          });
      if (newBookmark) {
        if (wantToClose && canClose) {
          browser.removeCurrentTab();
        }
        return true;
      }
      return false;
    },

    // Content area context menu: remove this page from stack
    remCurrentFromStack: function (aEvent) {
      // get the URL we want to drop
      var shelf = bookstack.stack.getShelf(),
          currentTab = bookstack.serv.getBrowser().mCurrentTab,
          currentURL = currentTab.linkedBrowser.webNavigation.currentURI,
          itemSet = [],
          doBatch,
          closure;
      if (!BMSVC.isBookmarked(currentURL)) {
        return;
      }
      // set up our query
      BMSVC.getBookmarkIdsForURI(currentURL, {}).forEach(function(aBookmark) {
        if (BMSVC.getFolderIdForItem(aBookmark) === shelf) {
          itemSet.push(aBookmark);
        }
      });
      doBatch = {
        runBatched: function (aUserData) {
          var closed = aUserData.wrappedJSObject;
          closed.items.forEach(BMSVC.removeItem);
        }
      };
      closure = { items: itemSet };
      closure.wrappedJSObject = closure;
      try {
        BMSVC.runInBatchMode(doBatch, closure);
      }
      catch (ex) {}
    },

    // Tab context menu: Add the current tab to stack.
    pushThisTab: function (aEvent) {
      var browser = bookstack.serv.getBrowser(),
          tab = browser.mContextTab,
          aDocShell,
          url,
          URI,
          title,
          newBookmark;
      if (tab.localName !== "tab") {
        tab = browser.mCurrentTab;
      }
      aDocShell = tab.linkedBrowser.webNavigation;
      URI = aDocShell.currentURI;
      url = URI.spec;
      if (url === "about:blank") {
        return null;
      }
      try {
        title = aDocShell.document.title || url;
      }
      catch (ex) {
        title = url;
      }
      newBookmark = bookstack.stack.onAddToStackIndex({
        uri: URI,
        title: title,
        edit: aEvent.shiftKey
      });
      return newBookmark;
    },

    // Add all visible tabs to stack.
    pushAllTabs: function (aEvent) {
      if (aEvent.shiftKey)
        this.pushAllTabsAsFolder();
      else
        this.pushAllTabsIntoStack();
    },

    pushAllTabsIntoStack: function () {
      var browser = bookstack.serv.getBrowser(),
          tabPanels = browser.visibleTabs,
          closure,
          doBatch;
      closure = {
        tabCount: tabPanels.length,
        tabPanels: tabPanels
      };
      closure.wrappedJSObject = closure;
      // The wrappedJSObject wisdom was taught by Mook on #extdev
      doBatch = {
        runBatched: function (aUserData) {
          var closed = aUserData.wrappedJSObject,
              tabs = closed.tabPanels,
              index,
              url,
              tab,
              title,
              URI;
          for (index = closed.tabCount - 1; index >= 0; index -= 1) {
            tab = browser.getBrowserForTab(tabs[index]);
            URI = tab.webNavigation.currentURI;
            url = URI.spec;
            if (url !== "about:blank") { /* Skip blanks */
              try {
                title = tab.webNavigation.document.title || url;
              }
              catch (ex) {
                title = url;
              }
              BMSVC.insertBookmark(
                bookstack.stack.getShelf(), URI, BMSVC.DEFAULT_INDEX, title);
            }
          }
        }
      };
      try {
        BMSVC.runInBatchMode(doBatch, closure);
      }
      catch (ex) {}
    },

    // Add all visible tabs to stack in a new folder.
    pushAllTabsAsFolder: function () {
      let pages = PlacesCommandHook.uniqueCurrentPages;
      if (pages.length > 1) {
        let shelf = bookstack.stack.getShelf();
        let insertionPoint = new InsertionPoint(shelf, BMSVC.DEFAULT_INDEX,
                                                0, false, false);
        PlacesUIUtils.showBookmarkDialog({
          action: "add",
          type: "folder",
          URIList: pages,
          hiddenRows: ["folderPicker", "description"],
          defaultInsertionPoint: insertionPoint
        }, window);
      }
    },

    // Determines visibility of tab context menu items based on count/context.
    toggleStackItems: function (aEvent) {
      var browser = bookstack.serv.getBrowser(),
          tabPanels = browser.browsers,
          goodFirst = false,
          goodTabs = false,
          url = browser.mContextTab.linkedBrowser.webNavigation.currentURI.spec;
      goodTabs = tabPanels.some(function(aTab) {
        if (aTab.webNavigation.currentURI.spec !== "about:blank") {
          if (goodFirst) {
            return true;
          }
          goodFirst = true;
        }
        return false;
      });
      document.getElementById("bookstack-tabpush").setAttribute(
        "disabled", url === "about:blank");
      document.getElementById("bookstack-taball").setAttribute(
        "disabled", !goodTabs);
    },

    // Tests node for being appropriate target (Anchor, Area, Link)
    isALink: function (aNode) {
      return (aNode instanceof HTMLAnchorElement ||
              aNode instanceof HTMLAreaElement ||
              aNode instanceof HTMLLinkElement);
    },

    // Called by listener, to add links via clicks.
    usurpMiddleClick: function (aEvent) {
      var target = aEvent.target,
          linkNode,
          isModified = aEvent.ctrlKey || aEvent.metaKey,
          url,
          parent,
          text,
          newBookmark;
      if (aEvent.button === 1 && bookstack.pref.STACK_MIDCLICK.value() &&
         (isModified || !bookstack.pref.STACK_MODKEY.value())) {
        if (bookstackOverlay.isALink(target)) {
          if (target.hasAttribute("href")) {
            linkNode = target;
          }
          parent = target.parentNode;
          while (parent) {
            if (bookstackOverlay.isALink(parent) &&
                parent.hasAttribute("href")) {
              linkNode = parent;
            }
            parent = parent.parentNode;
          }
        }
        else {
          linkNode = aEvent.originalTarget;
          while (linkNode && !(linkNode instanceof HTMLAnchorElement)) {
            linkNode = linkNode.parentNode;
          }
        }
        // <a> cannot be nested.  So if we find an anchor without an
        // href, there is no useful <a> around the target
        if (linkNode && linkNode.hasAttribute("href")) {
          text = gatherTextUnder(linkNode);
          if (!text || !text.match(/\S/)) {
            text = linkNode.getAttribute("title");
            if (!text || !text.match(/\S/)) {
              text = linkNode.getAttribute("alt");
              if (!text || !text.match(/\S/)) {
                text = linkNode.linkURL;
              }
            }
          }
          url = linkNode.href;
          newBookmark = bookstack.stack.onAddToStackIndex({
            url: url,
            title: text,
            edit: aEvent.shiftKey
          });
          aEvent.preventDefault();
          aEvent.stopPropagation();
          if (newBookmark) {
            return true;
          }
        }
      }
      return false;
    },

    // Installs the button for easy access; only called on original install.
    // Installs to the add-on bar now, since that's not as disruptive.
    addBookstackButton: function () {
      var buttonId = "bookstack-side-button",
          addonBarId = "addon-bar",
          addonbar = document.getElementById(addonBarId),
          toolbox = document.getElementById("navigator-toolbox"),
          hasButton = false,
          toolbar,
          i,
          currentSet,
          newSet;
      // Added in Fx4, only add the button there
      if (addonbar) {
        // Just in case, we check for the button where it could be:
        //   already in the addon bar
        if (addonbar.currentSet.indexOf(buttonId) > -1) {
          hasBookstackButton = true;
        }
        //   already in some other toolbar
        for (i = 0; !hasButton && i < toolbox.childNodes.length; i += 1) {
          toolbar = toolbox.childNodes[i];
          if (toolbar.localName === "toolbar" &&
              toolbar.getAttribute("customizable") === "true" &&
              toolbar.currentSet.indexOf(buttonId) > -1) {
            hasButton = true;
          }
        }
        // No button in any toolbar, add it to the addon bar
        if (!hasButton) {
          currentSet = addonbar.currentSet.split(',');
          currentSet.push(buttonId);
          newSet = currentSet.join(',');
          addonbar.setAttribute("currentset", newSet);
          addonbar.currentSet = newSet;
          document.persist(addonBarId, "currentset");
          try {
            BrowserToolboxCustomizeDone(true);
          }
          catch (ex) {}
        }
      }
    },

    // Listener to add various items per window:
    // tab context items, middle-click handler, content area context
    addWindowItems: function () {
      bookstackOverlay.toggleMiddleClickListener();
      document.getElementById("contentAreaContextMenu").addEventListener(
        "popupshowing", bookstackOverlay.bookstackContext, false);
      window.removeEventListener(
        "load", bookstackOverlay.addWindowItems, false);
      gBrowser.tabContextMenu.addEventListener(
        "popupshowing", bookstackOverlay.toggleStackItems, false);
      bookstack.stack.addWindow(window);
    },

    // Updates the version number.
    versionHandler: function (aAddon) {
      var version = aAddon.version,
          stackVersion = bookstack.pref.VERSION.value();
      if (!stackVersion) {
        bookstack.pref.VERSION.persist(version);
        window.setTimeout(bookstackOverlay.addBookstackButton, 500);
      }
      else if (stackVersion !== version) { /* Existing install */
        bookstack.pref.VERSION.persist(version);
      }
    },

    //Check for new installation, upgrade, and other housekeeping
    onLoad: function () {
      var self = bookstackOverlay;
      if (self.loaded) {
        return;
      }
      self.loaded = true;
      bookstack.serv.bookstackAddon(self.versionHandler);
      bookstack.stack.getShelf();
      document.getElementById("menu_ToolsPopup").addEventListener(
        "popupshowing", self.bookstackTools, false);
      bookstack.stack.registerBMO(bookstack.stack.notificationBMObserver);
      bookstack.serv.WindowWatcher().registerNotification(
        bookstack.stack.windowWatchObserver);
      self.prefListenerSet = new bookstack.pref.PrefListener(
        function (aBranch, aName) {
          switch (aName) {
            case "stack_middleclick":
              self.toggleMiddleClickListener();
              break;
            case "buttonupdate":
              self.fromCommandLine();
              break;
          }
        });
      self.prefListenerSet.register();
      if (bookstack.pref.STACK_CMDLINE_URI.value()) {
        self.fromCommandLine();
      }
      self.ObserverLoad.unregister();
    },

    // Observes for new windows.
    ObserverLoad: {
      observe: function (aSubject, aTopic, aData) {
        window.setTimeout(bookstackOverlay.onLoad, 500);
      },

      register: function () {
        bookstack.serv.addObserver(
          bookstackOverlay.ObserverLoad,
          "sessionstore-windows-restored",
          false);
      },

      unregister: function () {
        bookstack.serv.removeObserver(
          bookstackOverlay.ObserverLoad, "sessionstore-windows-restored");
      }
    },

    // Observes for the whole application closing (not windows!).
    ObserverUnload: {
      observe: function (aSubject, aTopic, aData) {
        bookstack.stack.unload();
        this.unregister();
      },

      register: function () {
        bookstack.serv.addObserver(
          bookstackOverlay.ObserverUnload,
          "quit-application-requested",
          false);
      },

      unregister: function () {
        bookstack.serv.removeObserver(
          bookstackOverlay.ObserverUnload, "quit-application-requested");
      }
    }
  };
}());
bookstackOverlay.ObserverLoad.register();
bookstackOverlay.ObserverUnload.register();
window.addEventListener("load", bookstackOverlay.addWindowItems, false);

